From 425e2854c5997b2f701e4f214c2d5282e3a83655 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Sat, 16 Sep 2017 17:31:30 +0100
Subject: [PATCH] Add some new tag-based stats and a new force param to
 stats-update

---
 build/index.php           | 157 ++++++++++++++++++++++++++++++++++----
 core.php                  |   2 +-
 module_index.json         |   8 +-
 modules/feature-stats.php |  71 ++++++++++++++---
 modules/page-list.php     |  84 +++++++++++++++++++-
 5 files changed, 290 insertions(+), 32 deletions(-)

diff --git a/build/index.php b/build/index.php
index 995ebe0..6e0768a 100644
--- a/build/index.php
+++ b/build/index.php
@@ -1459,7 +1459,7 @@ class page_renderer
 		<footer>
 			<p>{footer-message}</p>
 			<p>Powered by Pepperminty Wiki v0.15-dev, which was built by <a href='//starbeamrainbowlabs.com/'>Starbeamrainbowlabs</a>. Send bugs to 'bugs at starbeamrainbowlabs dot com' or <a href='//github.com/sbrl/Pepperminty-Wiki' title='Github Issue Tracker'>open an issue</a>.</p>
-			<p>Your local friendly administrators are {admins-name-list}.</p>
+			<p>Your local friendly moderators are {admins-name-list}.</p>
 			<p>This wiki is managed by <a href='mailto:{admin-details-email}'>{admin-details-name}</a>.</p>
 		</footer>
 		{navigation-bar-bottom}
@@ -4304,19 +4304,21 @@ class search
 
 register_module([
 	"name" => "Statistics",
-	"version" => "0.1",
+	"version" => "0.2",
 	"author" => "Starbeamrainbowlabs",
 	"description" => "An extensible statistics calculation system. Comes with a range of built-in statistics, but can be extended by other modules too.",
 	"id" => "feature-stats",
 	"code" => function() {
-		global $settings;
+		global $settings, $env;
+		
 		/**
-		 * @api {get|post} ?action=stats-update Recalculate the wiki's statistics
-		 * @apiName UpdateStats
+		 * @api {get} ?action=stats Show wiki statistics
+		 * @apiName Stats
 		 * @apiGroup Utility
-		 * @apiPermission Administrator
-		 *
-		 * @apiParam	{string}	secret	POST only, optional. If you're not logged in, you can specify the wiki's sekret (find it in peppermint.json) using this parameter.
+		 * @apiPermission Anonymous
+		 * @since v0.15
+		 * @apiParam	{string}	format	Specify the format the data should be returned in. Supported formats: html (default), json.
+		 * @apiParam	{string}	stat	HTML format only. If specified the page for the stat with this id is sent instead of the list of scalar stats.
 		 */
 		
 		/*
@@ -4329,8 +4331,21 @@ register_module([
 		add_action("stats", function() {
 			global $settings, $statistic_calculators;
 			
+			$allowed_formats = [ "html", "json" ];
+			$format = $_GET["format"] ?? "html";
+			
+			if(!in_array($format, $allowed_formats)) {
+				http_response_code(400);
+				exit(page_renderer::render_main("Format error - $settings->sitename", "<p>Error: The format '$format' is not currently supported by $settings->sitename. Supported formats: " . implode(", ", $allowed_formats) . "."));
+			}
+			
 			$stats = stats_load();
 			
+			if($format == "json") {
+				header("content-type: application/json");
+				exit(json_encode($stats, JSON_PRETTY_PRINT));
+			}
+			
 			$stat_pages_list = "<a href='?action=stats'>Main</a> | ";
 			foreach($statistic_calculators as $stat_id => $stat_calculator) {
 				if($stat_calculator["type"] == "scalar")
@@ -4343,12 +4358,24 @@ register_module([
 				$stat_calculator = $statistic_calculators[$_GET["stat"]];
 				$content = "<h1>{$stat_calculator["name"]} - Statistics</h1>\n";
 				$content .= "<p>$stat_pages_list</p>\n";
-				$content .= $stat_calculator["render"]($stats->{$_GET["stat"]});
+				switch($stat_calculator["type"]) {
+					case "page-list":
+						if(!module_exists("page-list")) {
+							$content .= "<p>$settings->sitename doesn't current have the page listing module installed, so HTML rendering of this statistic is currently unavailable. Try <a href='mailto:" . hide_email($settings->admindetails_email) . "'>contacting $settings->admindetails_name</a>, $settings->sitename's administrator and asking then to install the <code>page-list</code> module.</p>";
+							break;
+						}
+						$content .= generate_page_list($stats->{$_GET["stat"]}->value);
+						break;
+					
+					case "page":
+						$content .= $stat_calculator["render"]($stats->{$_GET["stat"]});
+						break;
+				}
 			}
 			else
 			{
 				$content = "<h1>Statistics</h1>\n";
-				$content .= "<p>This page contains a selection of statistics about $settings->sitename's content. They are updated automatically about every " . trim(str_replace(["ago", "1 "], [""], human_time($settings->stats_update_interval))) . ", although $settings->sitename's local friendly moderators may update it earlier (you can see their names at the bottom of every page).</p>\n";
+				$content .= "<p>This page contains a selection of statistics about $settings->sitename's content. They are updated automatically about every " . trim(str_replace(["ago", "1 "], [""], human_time($settings->stats_update_interval))) . ", although $settings->sitename's local friendly moderators may update them earlier (you can see their names at the bottom of every page).</p>\n";
 				$content .= "<p>$stat_pages_list</p>\n";
 				
 				$content .= "<table class='stats-table'>\n";
@@ -4365,6 +4392,15 @@ register_module([
 			exit(page_renderer::render_main("Statistics - $settings->sitename", $content));
 		});
 		
+		/**
+		 * @api {get|post} ?action=stats-update Recalculate the wiki's statistics
+		 * @apiName UpdateStats
+		 * @apiGroup Utility
+		 * @apiPermission Administrator
+		 * @since v0.15
+		 * @apiParam	{string}	secret	POST only, optional. If you're not logged in, you can specify the wiki's sekret instead (find it in peppermint.json) using this parameter.
+		 * @apiParam	{bool}		force	Whether the statistics should be recalculated anyway - even if they have already recently been recalculated. Default: no. Supported values: yes, no.
+		 */
 		
 		/*
 		 * ███████ ████████  █████  ████████ ███████
@@ -4394,7 +4430,7 @@ register_module([
 			// Delete the old stats cache
 			unlink($paths->statsindex);
 			
-			update_statistics(true);
+			update_statistics(true, ($_GET["force"] ?? "no") == "yes");
 			header("content-type: application/json");
 			echo(file_get_contents($paths->statsindex) . "\n");
 		});
@@ -4470,14 +4506,25 @@ register_module([
 		]);
 		
 		// Perform an automatic recalculation of the statistics if needed
-		update_statistics(false);
+		if($env->action !== "stats-update")
+			update_statistics(false);
 	}
 ]);
 
-function update_statistics($update_all = false)
+/**
+ * Updates the wiki's statistics.
+ * @package feature-stats
+ * @param  boolean $update_all Whether all the statistics should be checked and recalculated, or just as many as we have time for according to the settings.
+ * @param  boolean $force      Whether we should recalculate statistics that don't currently require recalculating anyway.
+ */
+function update_statistics($update_all = false, $force = false)
 {
 	global $settings, $statistic_calculators;
 	
+	// Cleart he existing statistics if we are asked to recalculate them all
+	if($force)
+		stats_save(new stdClass());
+	
 	$stats = stats_load();
 	
 	$start_time = microtime(true);
@@ -6475,7 +6522,7 @@ register_module([
 
 register_module([
 	"name" => "Page list",
-	"version" => "0.10.3",
+	"version" => "0.10.4",
 	"author" => "Starbeamrainbowlabs",
 	"description" => "Adds a page that lists all the pages in the index along with their metadata.",
 	"id" => "page-list",
@@ -6577,6 +6624,88 @@ register_module([
 			exit(page_renderer::render("$tag - Tag List - $settings->sitename", $content));
 		});
 		
+		statistic_add([
+			"id" => "tag-count",
+			"name" => "Number of Tags",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				$all_tags = [];
+				foreach($pageindex as $page_entry) {
+					if(empty($page_entry->tags))
+						continue;
+						
+					foreach($page_entry->tags as $tag) {
+						if(!in_array($tag, $all_tags)) $all_tags[] = $tag;
+					}
+				}
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = count($all_tags);
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "tags-per-page",
+			"name" => "Average Number of Tags per Page",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				$tag_counts = [];
+				foreach($pageindex as $page_entry)
+					$tag_counts[] = count($page_entry->tags ?? []);
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = round(array_sum($tag_counts) / count($tag_counts), 3);
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "most-tags",
+			"name" => "Most tags on a single page",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				
+				$highest_tag_count = 0;
+				$highest_tag_page = "";
+				foreach($pageindex as $pagename => $page_entry) {
+					if(count($page_entry->tags ?? []) > $highest_tag_count) {
+						$highest_tag_count = count($page_entry->tags ?? []);
+						$highest_tag_page = $pagename;
+					}
+				}
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = "$highest_tag_count (<a href='?page=" . rawurlencode($highest_tag_page) . "'>" . htmlentities($highest_tag_page) . "</a>)";
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "untagged-pages",
+			"name" => "Untagged Pages",
+			"type" => "page-list",
+			"update" => function($old_data) {
+				global $pageindex;
+				
+				$untagged_pages = [];
+				foreach($pageindex as $pagename => $page_entry) {
+					if(empty($page_entry->tags) || count($page_entry->tags ?? []) == 0)
+						$untagged_pages[] = $pagename;
+				}
+				
+				sort($untagged_pages, SORT_STRING | SORT_FLAG_CASE);
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = $untagged_pages;
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		
 		add_help_section("30-all-pages-tags", "Listing pages and tags", "<p>All the pages and tags on $settings->sitename are listed on a pair of pages to aid navigation. The list of all pages on $settings->sitename can be found by clicking &quot;All Pages&quot; on the top bar. The list of all the tags currently in use can be found by clicking &quot;All Tags&quot; in the &quot;More...&quot; menu in the top right.</p>
 		<p>Each tag on either page can be clicked, and leads to a list of all pages that possess that particular tag.</p>
 		<p>Redirect pages are shown in italics. A page's last known editor is also shown next to each entry on a list of pages, along with the last known size (which should correct, unless it was changed outside of $settings->sitename) and the time since the last modification (hovering over this will show the exact time that the last modification was made in a tooltip).</p>");
diff --git a/core.php b/core.php
index 5a29d84..9a7adf5 100644
--- a/core.php
+++ b/core.php
@@ -1092,7 +1092,7 @@ class page_renderer
 		<footer>
 			<p>{footer-message}</p>
 			<p>Powered by Pepperminty Wiki {version}, which was built by <a href='//starbeamrainbowlabs.com/'>Starbeamrainbowlabs</a>. Send bugs to 'bugs at starbeamrainbowlabs dot com' or <a href='//github.com/sbrl/Pepperminty-Wiki' title='Github Issue Tracker'>open an issue</a>.</p>
-			<p>Your local friendly administrators are {admins-name-list}.</p>
+			<p>Your local friendly moderators are {admins-name-list}.</p>
 			<p>This wiki is managed by <a href='mailto:{admin-details-email}'>{admin-details-name}</a>.</p>
 		</footer>
 		{navigation-bar-bottom}
diff --git a/module_index.json b/module_index.json
index 548034e..879bb92 100755
--- a/module_index.json
+++ b/module_index.json
@@ -109,11 +109,11 @@
     },
     {
         "name": "Statistics",
-        "version": "0.1",
+        "version": "0.2",
         "author": "Starbeamrainbowlabs",
         "description": "An extensible statistics calculation system. Comes with a range of built-in statistics, but can be extended by other modules too.",
         "id": "feature-stats",
-        "lastupdate": 1505564746,
+        "lastupdate": 1505579268,
         "optional": false
     },
     {
@@ -190,11 +190,11 @@
     },
     {
         "name": "Page list",
-        "version": "0.10.3",
+        "version": "0.10.4",
         "author": "Starbeamrainbowlabs",
         "description": "Adds a page that lists all the pages in the index along with their metadata.",
         "id": "page-list",
-        "lastupdate": 1505513005,
+        "lastupdate": 1505577933,
         "optional": false
     },
     {
diff --git a/modules/feature-stats.php b/modules/feature-stats.php
index 864597f..cd65c8f 100644
--- a/modules/feature-stats.php
+++ b/modules/feature-stats.php
@@ -1,19 +1,21 @@
 <?php
 register_module([
 	"name" => "Statistics",
-	"version" => "0.1",
+	"version" => "0.2",
 	"author" => "Starbeamrainbowlabs",
 	"description" => "An extensible statistics calculation system. Comes with a range of built-in statistics, but can be extended by other modules too.",
 	"id" => "feature-stats",
 	"code" => function() {
-		global $settings;
+		global $settings, $env;
+		
 		/**
-		 * @api {get|post} ?action=stats-update Recalculate the wiki's statistics
-		 * @apiName UpdateStats
+		 * @api {get} ?action=stats Show wiki statistics
+		 * @apiName Stats
 		 * @apiGroup Utility
-		 * @apiPermission Administrator
-		 *
-		 * @apiParam	{string}	secret	POST only, optional. If you're not logged in, you can specify the wiki's sekret (find it in peppermint.json) using this parameter.
+		 * @apiPermission Anonymous
+		 * @since v0.15
+		 * @apiParam	{string}	format	Specify the format the data should be returned in. Supported formats: html (default), json.
+		 * @apiParam	{string}	stat	HTML format only. If specified the page for the stat with this id is sent instead of the list of scalar stats.
 		 */
 		
 		/*
@@ -26,8 +28,21 @@ register_module([
 		add_action("stats", function() {
 			global $settings, $statistic_calculators;
 			
+			$allowed_formats = [ "html", "json" ];
+			$format = $_GET["format"] ?? "html";
+			
+			if(!in_array($format, $allowed_formats)) {
+				http_response_code(400);
+				exit(page_renderer::render_main("Format error - $settings->sitename", "<p>Error: The format '$format' is not currently supported by $settings->sitename. Supported formats: " . implode(", ", $allowed_formats) . "."));
+			}
+			
 			$stats = stats_load();
 			
+			if($format == "json") {
+				header("content-type: application/json");
+				exit(json_encode($stats, JSON_PRETTY_PRINT));
+			}
+			
 			$stat_pages_list = "<a href='?action=stats'>Main</a> | ";
 			foreach($statistic_calculators as $stat_id => $stat_calculator) {
 				if($stat_calculator["type"] == "scalar")
@@ -40,12 +55,24 @@ register_module([
 				$stat_calculator = $statistic_calculators[$_GET["stat"]];
 				$content = "<h1>{$stat_calculator["name"]} - Statistics</h1>\n";
 				$content .= "<p>$stat_pages_list</p>\n";
-				$content .= $stat_calculator["render"]($stats->{$_GET["stat"]});
+				switch($stat_calculator["type"]) {
+					case "page-list":
+						if(!module_exists("page-list")) {
+							$content .= "<p>$settings->sitename doesn't current have the page listing module installed, so HTML rendering of this statistic is currently unavailable. Try <a href='mailto:" . hide_email($settings->admindetails_email) . "'>contacting $settings->admindetails_name</a>, $settings->sitename's administrator and asking then to install the <code>page-list</code> module.</p>";
+							break;
+						}
+						$content .= generate_page_list($stats->{$_GET["stat"]}->value);
+						break;
+					
+					case "page":
+						$content .= $stat_calculator["render"]($stats->{$_GET["stat"]});
+						break;
+				}
 			}
 			else
 			{
 				$content = "<h1>Statistics</h1>\n";
-				$content .= "<p>This page contains a selection of statistics about $settings->sitename's content. They are updated automatically about every " . trim(str_replace(["ago", "1 "], [""], human_time($settings->stats_update_interval))) . ", although $settings->sitename's local friendly moderators may update it earlier (you can see their names at the bottom of every page).</p>\n";
+				$content .= "<p>This page contains a selection of statistics about $settings->sitename's content. They are updated automatically about every " . trim(str_replace(["ago", "1 "], [""], human_time($settings->stats_update_interval))) . ", although $settings->sitename's local friendly moderators may update them earlier (you can see their names at the bottom of every page).</p>\n";
 				$content .= "<p>$stat_pages_list</p>\n";
 				
 				$content .= "<table class='stats-table'>\n";
@@ -62,6 +89,15 @@ register_module([
 			exit(page_renderer::render_main("Statistics - $settings->sitename", $content));
 		});
 		
+		/**
+		 * @api {get|post} ?action=stats-update Recalculate the wiki's statistics
+		 * @apiName UpdateStats
+		 * @apiGroup Utility
+		 * @apiPermission Administrator
+		 * @since v0.15
+		 * @apiParam	{string}	secret	POST only, optional. If you're not logged in, you can specify the wiki's sekret instead (find it in peppermint.json) using this parameter.
+		 * @apiParam	{bool}		force	Whether the statistics should be recalculated anyway - even if they have already recently been recalculated. Default: no. Supported values: yes, no.
+		 */
 		
 		/*
 		 * ███████ ████████  █████  ████████ ███████
@@ -91,7 +127,7 @@ register_module([
 			// Delete the old stats cache
 			unlink($paths->statsindex);
 			
-			update_statistics(true);
+			update_statistics(true, ($_GET["force"] ?? "no") == "yes");
 			header("content-type: application/json");
 			echo(file_get_contents($paths->statsindex) . "\n");
 		});
@@ -167,14 +203,25 @@ register_module([
 		]);
 		
 		// Perform an automatic recalculation of the statistics if needed
-		update_statistics(false);
+		if($env->action !== "stats-update")
+			update_statistics(false);
 	}
 ]);
 
-function update_statistics($update_all = false)
+/**
+ * Updates the wiki's statistics.
+ * @package feature-stats
+ * @param  boolean $update_all Whether all the statistics should be checked and recalculated, or just as many as we have time for according to the settings.
+ * @param  boolean $force      Whether we should recalculate statistics that don't currently require recalculating anyway.
+ */
+function update_statistics($update_all = false, $force = false)
 {
 	global $settings, $statistic_calculators;
 	
+	// Clear the existing statistics if we are asked to recalculate them all
+	if($force)
+		stats_save(new stdClass());
+	
 	$stats = stats_load();
 	
 	$start_time = microtime(true);
diff --git a/modules/page-list.php b/modules/page-list.php
index 293c13d..b5c9e3f 100644
--- a/modules/page-list.php
+++ b/modules/page-list.php
@@ -1,7 +1,7 @@
 <?php
 register_module([
 	"name" => "Page list",
-	"version" => "0.10.3",
+	"version" => "0.10.4",
 	"author" => "Starbeamrainbowlabs",
 	"description" => "Adds a page that lists all the pages in the index along with their metadata.",
 	"id" => "page-list",
@@ -103,6 +103,88 @@ register_module([
 			exit(page_renderer::render("$tag - Tag List - $settings->sitename", $content));
 		});
 		
+		statistic_add([
+			"id" => "tag-count",
+			"name" => "Number of Tags",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				$all_tags = [];
+				foreach($pageindex as $page_entry) {
+					if(empty($page_entry->tags))
+						continue;
+						
+					foreach($page_entry->tags as $tag) {
+						if(!in_array($tag, $all_tags)) $all_tags[] = $tag;
+					}
+				}
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = count($all_tags);
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "tags-per-page",
+			"name" => "Average Number of Tags per Page",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				$tag_counts = [];
+				foreach($pageindex as $page_entry)
+					$tag_counts[] = count($page_entry->tags ?? []);
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = round(array_sum($tag_counts) / count($tag_counts), 3);
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "most-tags",
+			"name" => "Most tags on a single page",
+			"type" => "scalar",
+			"update" => function($old_data) {
+				global $pageindex;
+				
+				$highest_tag_count = 0;
+				$highest_tag_page = "";
+				foreach($pageindex as $pagename => $page_entry) {
+					if(count($page_entry->tags ?? []) > $highest_tag_count) {
+						$highest_tag_count = count($page_entry->tags ?? []);
+						$highest_tag_page = $pagename;
+					}
+				}
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = "$highest_tag_count (<a href='?page=" . rawurlencode($highest_tag_page) . "'>" . htmlentities($highest_tag_page) . "</a>)";
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		statistic_add([
+			"id" => "untagged-pages",
+			"name" => "Untagged Pages",
+			"type" => "page-list",
+			"update" => function($old_data) {
+				global $pageindex;
+				
+				$untagged_pages = [];
+				foreach($pageindex as $pagename => $page_entry) {
+					if(empty($page_entry->tags) || count($page_entry->tags ?? []) == 0)
+						$untagged_pages[] = $pagename;
+				}
+				
+				sort($untagged_pages, SORT_STRING | SORT_FLAG_CASE);
+				
+				$result = new stdClass(); // value, state, completed
+				$result->value = $untagged_pages;
+				$result->completed = true;
+				return $result;
+			}
+		]);
+		
 		add_help_section("30-all-pages-tags", "Listing pages and tags", "<p>All the pages and tags on $settings->sitename are listed on a pair of pages to aid navigation. The list of all pages on $settings->sitename can be found by clicking &quot;All Pages&quot; on the top bar. The list of all the tags currently in use can be found by clicking &quot;All Tags&quot; in the &quot;More...&quot; menu in the top right.</p>
 		<p>Each tag on either page can be clicked, and leads to a list of all pages that possess that particular tag.</p>
 		<p>Redirect pages are shown in italics. A page's last known editor is also shown next to each entry on a list of pages, along with the last known size (which should correct, unless it was changed outside of $settings->sitename) and the time since the last modification (hovering over this will show the exact time that the last modification was made in a tooltip).</p>");
